'use strict'

import AtRule from './at-rule';
import Comment from './comment';
import Declaration from './declaration';
import Root from './root';
import Rule from './rule';
import tokenizer from './tokenize';

const SAFE_COMMENT_NEIGHBOR = {
    empty: true,
    space: true
}

function findLastWithPosition(tokens) {
    for (let i = tokens.length - 1; i >= 0; i--) {
        let token = tokens[i]
        let pos = token[3] || token[2]
        if (pos) {
            return pos
        }
    }
}

class Parser {
    constructor(input) {
        this.input = input

        this.root = new Root()
        this.current = this.root
        this.spaces = ''
        this.semicolon = false

        this.createTokenizer()
        this.root.source = { input, start: { column: 1, line: 1, offset: 0 } }
    }

    atrule(token) {
        let node = new AtRule()
        node.name = token[1].slice(1)
        if (node.name === '') {
            this.unnamedAtrule(node, token)
        }
        this.init(node, token[2])

        let type
        let prev
        let shift
        let last = false
        let open = false
        let params = []
        let brackets = []

        while (!this.tokenizer.endOfFile()) {
            token = this.tokenizer.nextToken()
            type = token[0]

            if (type === '(' || type === '[') {
                brackets.push(type === '(' ? ')' : ']')
            } else if (type === '{' && brackets.length > 0) {
                brackets.push('}')
            } else if (type === brackets[brackets.length - 1]) {
                brackets.pop()
            }

            if (brackets.length === 0) {
                if (type === ';') {
                    node.source.end = this.getPosition(token[2])
                    node.source.end.offset++
                    this.semicolon = true
                    break
                } else if (type === '{') {
                    open = true
                    break
                } else if (type === '}') {
                    if (params.length > 0) {
                        shift = params.length - 1
                        prev = params[shift]
                        while (prev && prev[0] === 'space') {
                            prev = params[--shift]
                        }
                        if (prev) {
                            node.source.end = this.getPosition(prev[3] || prev[2])
                            node.source.end.offset++
                        }
                    }
                    this.end(token)
                    break
                } else {
                    params.push(token)
                }
            } else {
                params.push(token)
            }

            if (this.tokenizer.endOfFile()) {
                last = true
                break
            }
        }

        node.raws.between = this.spacesAndCommentsFromEnd(params)
        if (params.length) {
            node.raws.afterName = this.spacesAndCommentsFromStart(params)
            this.raw(node, 'params', params)
            if (last) {
                token = params[params.length - 1]
                node.source.end = this.getPosition(token[3] || token[2])
                node.source.end.offset++
                this.spaces = node.raws.between
                node.raws.between = ''
            }
        } else {
            node.raws.afterName = ''
            node.params = ''
        }

        if (open) {
            node.nodes = []
            this.current = node
        }
    }

    checkMissedSemicolon(tokens) {
        let colon = this.colon(tokens)
        if (colon === false) {
            return
        }

        let founded = 0
        let token
        for (let j = colon - 1; j >= 0; j--) {
            token = tokens[j]
            if (token[0] !== 'space') {
                founded += 1
                if (founded === 2) {
                    break
                }
            }
        }
        // If the token is a word, e.g. `!important`, `red` or any other valid property's value.
        // Then we need to return the colon after that word token. [3] is the "end" colon of that word.
        // And because we need it after that one we do +1 to get the next one.
        throw this.input.error(
            'Missed semicolon',
            token[0] === 'word' ? token[3] + 1 : token[2]
        )
    }

    colon(tokens) {
        let brackets = 0
        let prev, token, type
        for (let [i, element] of tokens.entries()) {
            token = element
            type = token[0]

            if (type === '(') {
                brackets += 1
            }
            if (type === ')') {
                brackets -= 1
            }
            if (brackets === 0 && type === ':') {
                if (!prev) {
                    this.doubleColon(token)
                } else if (prev[0] === 'word' && prev[1] === 'progid') {
                    continue
                } else {
                    return i
                }
            }

            prev = token
        }
        return false
    }

    comment(token) {
        let node = new Comment()
        this.init(node, token[2])
        node.source.end = this.getPosition(token[3] || token[2])
        node.source.end.offset++

        let text = token[1].slice(2, -2)
        if (/^\s*$/.test(text)) {
            node.text = ''
            node.raws.left = text
            node.raws.right = ''
        } else {
            let match = text.match(/^(\s*)([^]*\S)(\s*)$/)
            node.text = match[2]
            node.raws.left = match[1]
            node.raws.right = match[3]
        }
    }

    createTokenizer() {
        this.tokenizer = tokenizer(this.input)
    }

    decl(tokens, customProperty) {
        let node = new Declaration()
        this.init(node, tokens[0][2])

        let last = tokens[tokens.length - 1]
        if (last[0] === ';') {
            this.semicolon = true
            tokens.pop()
        }

        node.source.end = this.getPosition(
            last[3] || last[2] || findLastWithPosition(tokens)
        )
        node.source.end.offset++

        while (tokens[0][0] !== 'word') {
            if (tokens.length === 1) {
                this.unknownWord(tokens)
            }
            node.raws.before += tokens.shift()[1]
        }
        node.source.start = this.getPosition(tokens[0][2])

        node.prop = ''
        while (tokens.length) {
            let type = tokens[0][0]
            if (type === ':' || type === 'space' || type === 'comment') {
                break
            }
            node.prop += tokens.shift()[1]
        }

        node.raws.between = ''

        let token
        while (tokens.length) {
            token = tokens.shift()

            if (token[0] === ':') {
                node.raws.between += token[1]
                break
            } else {
                if (token[0] === 'word' && /\w/.test(token[1])) {
                    this.unknownWord([token])
                }
                node.raws.between += token[1]
            }
        }

        if (node.prop[0] === '_' || node.prop[0] === '*') {
            node.raws.before += node.prop[0]
            node.prop = node.prop.slice(1)
        }

        let firstSpaces = []
        let next
        while (tokens.length) {
            next = tokens[0][0]
            if (next !== 'space' && next !== 'comment') {
                break
            }
            firstSpaces.push(tokens.shift())
        }

        this.precheckMissedSemicolon(tokens)

        for (let i = tokens.length - 1; i >= 0; i--) {
            token = tokens[i]
            if (token[1].toLowerCase() === '!important') {
                node.important = true
                let string = this.stringFrom(tokens, i)
                string = this.spacesFromEnd(tokens) + string
                if (string !== ' !important') {
                    node.raws.important = string
                }
                break
            } else if (token[1].toLowerCase() === 'important') {
                let cache = tokens.slice(0)
                let str = ''
                for (let j = i; j > 0; j--) {
                    let type = cache[j][0]
                    if (str.trim().startsWith('!') && type !== 'space') {
                        break
                    }
                    str = cache.pop()[1] + str
                }
                if (str.trim().startsWith('!')) {
                    node.important = true
                    node.raws.important = str
                    tokens = cache
                }
            }

            if (token[0] !== 'space' && token[0] !== 'comment') {
                break
            }
        }

        let hasWord = tokens.some(i => i[0] !== 'space' && i[0] !== 'comment')

        if (hasWord) {
            node.raws.between += firstSpaces.map(i => i[1]).join('')
            firstSpaces = []
        }
        this.raw(node, 'value', firstSpaces.concat(tokens), customProperty)

        if (node.value.includes(':') && !customProperty) {
            this.checkMissedSemicolon(tokens)
        }
    }

    doubleColon(token) {
        throw this.input.error(
            'Double colon',
            { offset: token[2] },
            { offset: token[2] + token[1].length }
        )
    }

    emptyRule(token) {
        let node = new Rule()
        this.init(node, token[2])
        node.selector = ''
        node.raws.between = ''
        this.current = node
    }

    end(token) {
        if (this.current.nodes && this.current.nodes.length) {
            this.current.raws.semicolon = this.semicolon
        }
        this.semicolon = false

        this.current.raws.after = (this.current.raws.after || '') + this.spaces
        this.spaces = ''

        if (this.current.parent) {
            this.current.source.end = this.getPosition(token[2])
            this.current.source.end.offset++
            this.current = this.current.parent
        } else {
            this.unexpectedClose(token)
        }
    }

    endFile() {
        if (this.current.parent) {
            this.unclosedBlock()
        }
        if (this.current.nodes && this.current.nodes.length) {
            this.current.raws.semicolon = this.semicolon
        }
        this.current.raws.after = (this.current.raws.after || '') + this.spaces
        this.root.source.end = this.getPosition(this.tokenizer.position())
    }

    freeSemicolon(token) {
        this.spaces += token[1]
        if (this.current.nodes) {
            let prev = this.current.nodes[this.current.nodes.length - 1]
            if (prev && prev.type === 'rule' && !prev.raws.ownSemicolon) {
                prev.raws.ownSemicolon = this.spaces
                this.spaces = ''
            }
        }
    }

    // Helpers

    getPosition(offset) {
        let pos = this.input.fromOffset(offset)
        return {
            column: pos.col,
            line: pos.line,
            offset
        }
    }

    init(node, offset) {
        this.current.push(node)
        node.source = {
            input: this.input,
            start: this.getPosition(offset)
        }
        node.raws.before = this.spaces
        this.spaces = ''
        if (node.type !== 'comment') {
            this.semicolon = false
        }
    }

    other(start) {
        let end = false
        let type = null
        let colon = false
        let bracket = null
        let brackets = []
        let customProperty = start[1].startsWith('--')

        let tokens = []
        let token = start
        while (token) {
            type = token[0]
            tokens.push(token)

            if (type === '(' || type === '[') {
                if (!bracket) {
                    bracket = token
                }
                brackets.push(type === '(' ? ')' : ']')
            } else if (customProperty && colon && type === '{') {
                if (!bracket) {
                    bracket = token
                }
                brackets.push('}')
            } else if (brackets.length === 0) {
                if (type === ';') {
                    if (colon) {
                        this.decl(tokens, customProperty)
                        return
                    } else {
                        break
                    }
                } else if (type === '{') {
                    this.rule(tokens)
                    return
                } else if (type === '}') {
                    this.tokenizer.back(tokens.pop())
                    end = true
                    break
                } else if (type === ':') {
                    colon = true
                }
            } else if (type === brackets[brackets.length - 1]) {
                brackets.pop()
                if (brackets.length === 0) {
                    bracket = null
                }
            }

            token = this.tokenizer.nextToken()
        }

        if (this.tokenizer.endOfFile()) {
            end = true
        }
        if (brackets.length > 0) {
            this.unclosedBracket(bracket)
        }

        if (end && colon) {
            if (!customProperty) {
                while (tokens.length) {
                    token = tokens[tokens.length - 1][0]
                    if (token !== 'space' && token !== 'comment') {
                        break
                    }
                    this.tokenizer.back(tokens.pop())
                }
            }
            this.decl(tokens, customProperty)
        } else {
            this.unknownWord(tokens)
        }
    }

    parse() {
        let token
        while (!this.tokenizer.endOfFile()) {
            token = this.tokenizer.nextToken()

            switch (token[0]) {
                case 'space':
                    this.spaces += token[1]
                    break

                case ';':
                    this.freeSemicolon(token)
                    break

                case '}':
                    this.end(token)
                    break

                case 'comment':
                    this.comment(token)
                    break

                case 'at-word':
                    this.atrule(token)
                    break

                case '{':
                    this.emptyRule(token)
                    break

                default:
                    this.other(token)
                    break
            }
        }
        this.endFile()
    }

    precheckMissedSemicolon( /* tokens */) {
        // Hook for Safe Parser
    }

    raw(node, prop, tokens, customProperty) {
        let token, type
        let length = tokens.length
        let value = ''
        let clean = true
        let next, prev

        for (let i = 0; i < length; i += 1) {
            token = tokens[i]
            type = token[0]
            if (type === 'space' && i === length - 1 && !customProperty) {
                clean = false
            } else if (type === 'comment') {
                prev = tokens[i - 1] ? tokens[i - 1][0] : 'empty'
                next = tokens[i + 1] ? tokens[i + 1][0] : 'empty'
                if (!SAFE_COMMENT_NEIGHBOR[prev] && !SAFE_COMMENT_NEIGHBOR[next]) {
                    if (value.slice(-1) === ',') {
                        clean = false
                    } else {
                        value += token[1]
                    }
                } else {
                    clean = false
                }
            } else {
                value += token[1]
            }
        }
        if (!clean) {
            let raw = tokens.reduce((all, i) => all + i[1], '')
            node.raws[prop] = { raw, value }
        }
        node[prop] = value
    }

    rule(tokens) {
        tokens.pop()

        let node = new Rule()
        this.init(node, tokens[0][2])

        node.raws.between = this.spacesAndCommentsFromEnd(tokens)
        this.raw(node, 'selector', tokens)
        this.current = node
    }

    spacesAndCommentsFromEnd(tokens) {
        let lastTokenType
        let spaces = ''
        while (tokens.length) {
            lastTokenType = tokens[tokens.length - 1][0]
            if (lastTokenType !== 'space' && lastTokenType !== 'comment') {
                break
            }
            spaces = tokens.pop()[1] + spaces
        }
        return spaces
    }

    // Errors

    spacesAndCommentsFromStart(tokens) {
        let next
        let spaces = ''
        while (tokens.length) {
            next = tokens[0][0]
            if (next !== 'space' && next !== 'comment') {
                break
            }
            spaces += tokens.shift()[1]
        }
        return spaces
    }

    spacesFromEnd(tokens) {
        let lastTokenType
        let spaces = ''
        while (tokens.length) {
            lastTokenType = tokens[tokens.length - 1][0]
            if (lastTokenType !== 'space') {
                break
            }
            spaces = tokens.pop()[1] + spaces
        }
        return spaces
    }

    stringFrom(tokens, from) {
        let result = ''
        for (let i = from; i < tokens.length; i++) {
            result += tokens[i][1]
        }
        tokens.splice(from, tokens.length - from)
        return result
    }

    unclosedBlock() {
        let pos = this.current.source.start
        throw this.input.error('Unclosed block', pos.line, pos.column)
    }

    unclosedBracket(bracket) {
        throw this.input.error(
            'Unclosed bracket',
            { offset: bracket[2] },
            { offset: bracket[2] + 1 }
        )
    }

    unexpectedClose(token) {
        throw this.input.error(
            'Unexpected }',
            { offset: token[2] },
            { offset: token[2] + 1 }
        )
    }

    unknownWord(tokens) {
        throw this.input.error(
            'Unknown word',
            { offset: tokens[0][2] },
            { offset: tokens[0][2] + tokens[0][1].length }
        )
    }

    unnamedAtrule(node, token) {
        throw this.input.error(
            'At-rule without name',
            { offset: token[2] },
            { offset: token[2] + token[1].length }
        )
    }
}

export { Parser }

export default Parser
